package inglar.autotc.client.ui;

import inglar.autotc.client.HasContainingTreeItem;
import inglar.autotc.client.NodeLookup;
import inglar.autotc.client.resource.ResourceHandler;
import inglar.autotc.shared.Values;

import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.HasVerticalAlignment;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.TreeItem;
import com.google.gwt.user.client.ui.VerticalPanel;

/**
 * A renderable representation of a tc qdisc. 
 * @author Lee
 */
public class NodeQDisc extends Composite implements HasContainingTreeItem {
	
//	private String script = "initscript";
	
	private String handle = "inithandle";
	private String parent = "initparent";
	
	private String qdisc = "initqdisc";
	private String options = "initopts opts";
	
	private String device = "initdevice";
	private String action = "initaction";
	
	private HorizontalPanel contentPanel;
	private Image qicon;
	private HTML lblHandle;
	private HTML lblParent;
	private HTML lblQDisc;
	
	private VerticalPanel varopts;
	private HorizontalPanel buttons;
	
	public Button btnDelete;
	public Button btnModify;
	public Button btnAddClass;
	public Button btnAddFilter;
	
	PageDevices tsRef = null;
	NodeQDisc self = null;
	private TreeItem container = null;
	
	public NodeQDisc() {
		
		contentPanel = new HorizontalPanel();
		initWidget(contentPanel);
		contentPanel.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_CENTER);
		contentPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);
		
		qicon = new Image(ResourceHandler.INSTANCE.qgreen16());
		contentPanel.add(qicon);
		qicon.getElement().setClassName("nodeIcon");
		
		VerticalPanel p1 = new VerticalPanel();
		contentPanel.add(p1);
		p1.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_LEFT);
		p1.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);
		
		lblQDisc = new HTML();
		p1.add(lblQDisc);
		lblQDisc.getElement().setClassName("tsNodeLabel");
		
		lblHandle = new HTML();
		p1.add(lblHandle);
		lblHandle.getElement().setClassName("tsNodeLabel");
		
		lblParent = new HTML();
		p1.add(lblParent);
		lblParent.getElement().setClassName("tsNodeLabel");
		
		varopts = new VerticalPanel();
		contentPanel.add(varopts);
		varopts.getElement().setAttribute("style", "margin-left: 15px;");
		
		buttons = new HorizontalPanel();
		contentPanel.add(buttons);
		buttons.getElement().setAttribute("style", "margin-left: 15px;");

		VerticalPanel p2 = new VerticalPanel();
		buttons.add(p2);
		
		btnDelete = new Button("DELETE");
		p2.add(btnDelete);
		btnDelete.addClickHandler(new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				if(getParentId().equals("root")) {
					tsRef.tree.clear();
					NodeQDisc nq = getDefaultRoot(getDevice(), tsRef);
					nq.setContainer(tsRef.tree.addItem(nq));
				} else {
					container.remove();
				}
			}
		});
		btnDelete.getElement().setAttribute("style", "font-weight: bold; font-size: xx-small;");
		
		btnModify = new Button("MODIFY");
		p2.add(btnModify);
		btnModify.addClickHandler(new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				DialogBoxEditNodeQDisc nqe = new DialogBoxEditNodeQDisc();
				nqe.setTargetNode(self);
				nqe.center();
				if(isQDiscClassful(getQdisc())) nqe.listQDisc.setEnabled(false);
				nqe.txtbxParent.setEnabled(false);
				nqe.txtbxHandle.setEnabled(false);
				String q = getQdisc();
				if(q.equals("htb")) {
//					nqe.txtbxHtbDefault.setEnabled(false);
				}
				if(q.equals("prio")) {
					nqe.intbxPrioBands.setEnabled(false);
				}
			}
		});
		btnModify.getElement().setAttribute("style", "font-weight: bold; font-size: xx-small;");
		
		VerticalPanel p3 = new VerticalPanel();
		buttons.add(p3);
		
		btnAddClass = new Button("ADD CLASS");
		p3.add(btnAddClass);
		btnAddClass.addClickHandler(new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				if(getQdisc().equals("htb")) {
					int children = container.getChildCount();
					int maxm = 0;
					for(int i = 0; i < children; i++) {
						NodeClass nc = (NodeClass) container.getChild(i).getWidget();
						String minor = NodeLookup.getMinor(nc.getClassid());
						int m = Integer.parseInt(minor, 16);
						if(m > maxm) maxm = m;
					}
					String major = NodeLookup.getMinor(getHandle());
					if(major.equals("0")) major = "1";
					String classid = major + ":" + Integer.toHexString(maxm + 1);
					String device = getDevice();
					String handle = getHandle();
					String tc =  "tc class add dev " + device + " parent " + handle + " classid " + classid + " htb rate 1bps";
					NodeClass nc = new NodeClass(tc, tsRef);
					TreeItem c = container.addItem(nc);
					nc.setContainer(c);
					container.setState(true);
				} else {
					Window.alert("Adding classes to this qdisc is not supported.");
				}
			}
		});
		btnAddClass.getElement().setAttribute("style", "font-weight: bold; font-size: xx-small;");
		
		btnAddFilter = new Button("ADD FILTER");
		p3.add(btnAddFilter);
		btnAddFilter.addClickHandler(new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				NodeFilter nf = new NodeFilter("tc filter add dev " + device + " pref 1 protocol ip parent " + handle + " flowid " + handle);
				nf.setContainer(container.addItem(nf));
				container.setState(true);
			}
		});
		btnAddFilter.getElement().setAttribute("style", "font-weight: bold; font-size: xx-small;");
		
		this.getElement().setClassName("tsNodeQDisc");
		
		self = this;
	}

	public NodeQDisc(String tcCmd, PageDevices ref) {
		this();
		if(ref == null) throw new IllegalArgumentException("Argument ref must not be null");
		this.setScript(tcCmd);
		this.tsRef = ref;
	}

	/**
	 * @return the handle
	 */
	public String getHandle() {
		return handle;
	}

	/**
	 * @param handle the handle to set
	 */
	public void setHandle(String handle) {
		this.handle = handle;
		lblHandle.setHTML("handle <b>" + getHandle() + "</b>");
	}

	/**
	 * @return the parent
	 */
	public String getParentId() {
		return parent;
	}

	/**
	 * @param parent the parent to set
	 */
	public void setParentId(String parentId) {
		this.parent = parentId;
		if(parentId.equals("root")) lblParent.setHTML("at <b>root</b>");
		else lblParent.setHTML("parent <b>" + getParentId() + "</b>");
	}

	/**
	 * @return the qdisc
	 */
	public String getQdisc() {
		return qdisc;
	}

	/**
	 * @param qdisc the qdisc to set
	 */
	public void setQdisc(String qdisc) {
		if(!isQDisc(qdisc)) throw new IllegalArgumentException("Argument qdisc (" + qdisc + ") is not a supported qdisc");
		else {
			this.qdisc = qdisc;
			lblQDisc.setHTML("qdisc <b>" + qdisc + "</b>");
			if(NodeQDisc.isQDiscClassful(qdisc) && !qdisc.equals("prio")) { // predetermined child classes for prio
				btnAddClass.setEnabled(true);
			} else {
				btnAddClass.setEnabled(false);
			}
			if(NodeQDisc.isQDiscClassless(this.qdisc)) {
				btnAddFilter.setEnabled(false);
			} else {
				btnAddFilter.setEnabled(true);
			}
		}
	}

	/**
	 * @return the options
	 */
	public String getOptions() {
		return options;
	}

	/**
	 * @param options the options to set
	 */
	public void setOptions(String str) {
//		this.options = str;
		str = str.trim();
		varopts.clear();
		options = str;
		if(str.isEmpty()) return;
		String[] opts = str.split(Values.REGEX_ALL_WHITESPACE);
		if(this.getQdisc().equals("prio")) {
			int idxBands = -1, idxMap = -1;
			for(int i = 0; i < opts.length; i++) {
				if(opts[i].equals("bands")) idxBands = i;
				if(opts[i].equals("priomap")) idxMap = i;
			}
			if(idxBands != -1) {
				String temp = opts[idxBands] + " <b>" + opts[idxBands + 1] + "</b>";
				HTML html = new HTML(temp);
				varopts.add(html);
				html.getElement().setClassName("tsNodeLabel");
				options = opts[idxBands] + " " + opts[idxBands + 1] + " ";
			}
			if(idxMap != -1) {
				String temp = opts[idxMap] + " ";
				int j = 0;
				for(int i = idxMap + 1; i < opts.length; i++) {
					temp += opts[i] + " ";
					j++;
				}
				if(j == 16) 
				{
					HTML html = new HTML(temp.trim());
					varopts.add(html);
					html.getElement().setClassName("tsNodeLabel");
					options += temp.trim();
				}
			}
			options = options.trim();
		} else {
			for(int i = 0; i < opts.length; i += 2) {
				String temp = opts[i] + " <b>" + opts[i + 1] + "</b>";
				HTML html = new HTML(temp);
				varopts.add(html);
				html.getElement().setClassName("tsNodeLabel");
			}
			options = str;
		}
	}

	/**
	 * @return the device
	 */
	public String getDevice() {
		return device;
	}

	/**
	 * @param device the device to set
	 */
	public void setDevice(String device) {
		this.device = device;
	}
	
	/**
	 * @return the action
	 */
	public String getAction() {
		return action;
	}

	/**
	 * @param action the action to set
	 */
	public void setAction(String action) {
		this.action = action;
	}

	/**
	 * @return the script
	 */
	public String getScript() {
		if(!getQdisc().equals("pfifo_fast")) {
			String retval = "tc qdisc ";
			retval += getAction() + " dev "; // action
			retval += this.getDevice() + " "; // device
			String t = this.getParentId();
			if(!t.equals("root") && !t.equals("ingress")) retval += "parent ";
			retval += t + " ";
			retval += "handle " + this.getHandle() + " "; // handle
			retval += this.getQdisc() + " "; // qdisc
			retval += this.getOptions(); // options
			return retval.trim();
		} else {
			if(getParentId().equals("root")) return "";
					//"tc qdisc del dev " + getDevice() + " root";
			else return "";
		}
	}
	
	/**
	 * @param str the script to set
	 */
	public void setScript(String str) {
		String script = str.trim();
		
		String[] v = script.split(Values.REGEX_ALL_WHITESPACE);
		int i = 0;
		if(!v[i++].equals("tc")) throw new IllegalArgumentException("Argument script not a tc command");
		if(!v[i++].equals("qdisc")) throw new IllegalArgumentException("Argument script not a tc qdisc command");
		
		if(!v[i].equals("dev")) {
			String temp = v[i++];
			if(temp.equals("add")
					|| temp.equals("del") || temp.equals("replace") //|| temp.equals("change") || temp.equals("show")
					) this.setAction(temp);
			else throw new IllegalArgumentException("Argument script has an invalid or unwanted optional parameter (" + temp + ") before dev");
		}
		
		if(!v[i++].equals("dev")) throw new IllegalArgumentException("Argument script does not have a dev parameter where expected");
		if(v[i].equals("handle") || v[i].equals("root") || v[i].equals("ingress") || v[i].equals("parent")) throw new IllegalArgumentException("Argument script has unsafe dev parameter value");
		else this.setDevice(v[i++]);
		
		int posParent = -1, posHandle = -1; 
		boolean getParent = false, getHandle = false;
		for( ; i < v.length; i++) {
			if(v[i].equals("root")) {
				if(posParent < 0) {
					if(getParent || getHandle) throw new IllegalArgumentException("Argument script has unexpected token ('root')");
					this.setParentId(v[i]);
					posParent = i;
//					this.setAction("replace");
				} else throw new IllegalArgumentException("Argument script already has parent or root attachment");
			} else if(v[i].equals("parent")) {
				if(posParent < 0) {
					if(getParent || getHandle) throw new IllegalArgumentException("Argument script has unexpected token ('parent')");
					getParent = true;
					posParent = i;
				} else throw new IllegalArgumentException("Argument script already has parent or root attachment");
			} else if(v[i].equals("handle")) {
				if(posHandle < 0) {
					if(getParent || getHandle) throw new IllegalArgumentException("Argument script has unexpected token ('handle')");
					getHandle = true;
					posHandle = i;
				} else throw new IllegalArgumentException("Argument script already has handle");
			} else {
				if(getParent) {
					this.setParentId(v[i]);
					getParent = false;
				} else if(getHandle) {
					this.setHandle(v[i]);
					getHandle = false;
				} else break; // i must be pointing to QDISC_KIND
			}
		}
		
		// here, i must be pointing to QDISC_KIND
		this.setQdisc(v[i++]); // already throws IAE if QDISC_KIND is unknown
		
		// XXX NodeQDisc.setScript doesn't care for OPTIONS
		String opts = "";
		for(int j = i; j < v.length; j++) opts += v[j] + " ";
		this.setOptions(opts.trim());
//		this.script = script;
	}
	
	/**
	 * List of tc classless QDiscs. Existing qdiscs which are not supported by the application are not included here.
	 */
	public static String[] qdiscs_classless = {
			"bfifo",
			"pfifo",
			"sfq",
//			"red",
//			"esfq",
//			"gred",
			"tbf",
			"pfifo_fast" // default qdisc
		};
	
	/**
	 * List of tc classful QDiscs. Existing qdiscs which are not supported by the application are not included here.
	 */
	public static String[] qdiscs_classful = {
			"prio",
//			"hfsc",
//			"cbq",
			"htb"
		};
	
	/**
	 * Determines whether a given string is a QDisc supported by the application. 
	 * @param str qdisc name
	 * @return <b>true</b> when parameter is a supported QDisc; <b>false</b> otherwise
	 */
	public static boolean isQDisc(String str) {
		return (isQDiscClassful(str) || isQDiscClassless(str));
	}
	
	/**
	 * Determines whether a given string is a classful QDisc supported by the application.
	 * @param str qdisc name
	 * @return <b>true</b> when parameter is a supported classful QDisc; <b>false</b> otherwise
	 */
	public static boolean isQDiscClassful(String str) {
		for(int i = 0; i < qdiscs_classful.length; i++) {
			if(qdiscs_classful[i].equals(str)) return true;
		}
		return false;
	}
	
	/**
	 * Determines whether a given string is a classless QDisc supported by the application.
	 * @param str qdisc name
	 * @return <b>true</b> when parameter is a supported classless QDisc; <b>false</b> otherwise
	 */
	public static boolean isQDiscClassless(String str) {
		for(int i = 0; i < qdiscs_classless.length; i++) {
			if(qdiscs_classless[i].equals(str)) return true;
		}
		return false;
	}
	
	public static NodeQDisc getDefaultRoot(String device, PageDevices ts) {
		return new NodeQDisc("tc qdisc add dev " + device + " root handle 1: pfifo_fast", ts);
	}

	/**
	 * @return the container
	 */
	public TreeItem getContainer() {
		return container;
	}

	/**
	 * @param container the container to set
	 */
	public void setContainer(TreeItem container) {
		this.container = container;
		
		if(this.getQdisc().equals("prio")) {
			int numBands = 3;
			String[] u = this.getOptions().trim().split(Values.REGEX_ALL_WHITESPACE);
			int idxBands = -1;
			for(int i = 0; i < u.length; i++) {
				if(u[i].equals("bands")) idxBands = i;
			}
			if(idxBands != -1) {
				Double d = DialogBoxEditNodeQDisc.getBaseDouble(u[idxBands + 1]);
				if(d != null) {
					Integer v = d.intValue();
					if(v != null) numBands = v.intValue();
				}
			}
			for(int i = 0; i < numBands; i++) {
				NodeClass nc = NodeClass.getPseudoClassPrio(this.getDevice(), this.getHandle(), (i + 1) + "", tsRef);
				TreeItem ti = container.addItem(nc);
				nc.setContainer(ti);
				nc.btnAddFilter.setEnabled(false);
				nc.btnAddClass.setEnabled(false);
				nc.btnModify.setEnabled(false);
				nc.btnDelete.setEnabled(false);
			}
		}
		
		container.setState(true);
	}
	
}

/*
# tc qdisc help
Usage: tc qdisc [ add | del | replace | change | show ] dev STRING
       [ handle QHANDLE ] [ root | ingress | parent CLASSID ] 
------ [ estimator INTERVAL TIME_CONSTANT ] ----------------------------------- assumed not in script
------ [ stab [ help | STAB_OPTIONS] ] ---------------------------------------- assumed not in script
       [ [ QDISC_KIND ] [ help | OPTIONS ] ]

       tc qdisc show [ dev STRING ] [ingress]
Where:
QDISC_KIND := { [p|b]fifo | tbf | prio | cbq | red | etc. }
OPTIONS := ... try tc qdisc add <desired QDISC_KIND> help
STAB_OPTIONS := ... try tc qdisc add stab help
#

 123456789 123456789
tc qdisc replace dev eth0 root handle 1: htb default 30;
tc class add dev eth0 parent 1: classid 1:1 htb rate 6mbit burst 15k;
tc class add dev eth0 parent 1: classid 1:10 htb rate 5mbit burst 15k;
tc class add dev eth0 parent 1: classid 1:20 htb rate 3mbit ceil 6mbit burst 15k;
tc class add dev eth0 parent 1: classid 1:30 htb rate 1kbit ceil 6mbit burst 15k;
tc qdisc add dev eth0 parent 1:10 handle 10: bfifo limit 1024;
tc qdisc add dev eth0 parent 1:20 handle 20: pfifo limit 20;
tc qdisc add dev eth0 parent 1:30 handle 30: sfq perturb 10;
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip dport 80 0xffff flowid 1:10;
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip sport 25 0xffff match ip dst 4.3.2.0/24 flowid 1:20;

*/